package scanner;

import compiler.Properties;
import parser.TokenType;
import util.Arquivo;

/**
 * Scanner class
 * @version 2010-september-04
 * @discipline Compiladores
 * @author Gustavo H P Carvalho
 * @email gustavohpcarvalho@ecomp.poli.br
 */
public class Scanner {

	// The file object that will be used to read the source code
	private Arquivo file;
	// The last char read from the source code
	private char currentChar;
	// The kind of the current token
	private int currentKind;
	// Buffer to append characters read from file
	private StringBuffer currentSpelling;
	// Current line and column in the source file
	private int line, column;
	
	/**
	 * Default constructor
	 */
	public Scanner() {
		this.file = new Arquivo(Properties.sourceCodeLocation);		
		this.line = 0;
		this.column = 0;
		this.currentChar = this.file.readChar();
		this.currentSpelling = new StringBuffer();
	}
	
	/**
	 * Returns the next token
	 * @return
	 */ //TODO
	public Token getNextToken() throws LexicalException{
		// Initializes the string buffer
		//this.currentSpelling.delete(0, this.currentSpelling.length());
		// Ignores separators
		scanSeparator();
		// Clears the string buffer
		this.currentSpelling.delete(0, this.currentSpelling.length());
		// Scans the next token
		this.currentKind = scanToken();
		// Creates and returns a token for the lexema identified
		return new Token(this.currentKind, this.currentSpelling.toString(), this.line, this.column);
	}
	
	/**
	 * Returns if a character is a separator
	 * @param c
	 * @return
	 */
	private boolean isSeparator(char c) {
		if ( c == '#' || c == ' ' || c == '\n' || c == '\t' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Reads (and ignores) a separator
	 * @throws LexicalException
	 */ //TODO
	private void scanSeparator() throws LexicalException {
		// A command line should finish with a \n
		while(isSeparator(this.currentChar)){
			// If it is a comment line
			if(this.currentChar == '#'){
				while(this.currentChar != '\n'){
					getNextChar();
				}
			}
			// Gets next char
			getNextChar();
		}
	}
	
	/**
	 * Gets the next char
	 */
	private void getNextChar() {
		// Appends the current char to the string buffer
		this.currentSpelling.append(this.currentChar);
		// Reads the next one
		this.currentChar = this.file.readChar();
		// Increments the line and column
		this.incrementLineColumn();
	}
	
	/**
	 * Increments line and column
	 */
	private void incrementLineColumn() {
		// If the char read is a '\n', increments the line variable and assigns 0 to the column
		if ( this.currentChar == '\n' ) {
			this.line++;
			this.column = 0;
		// If the char read is not a '\n' 
		} else {
			// If it is a '\t', increments the column by 4
			if ( this.currentChar == '\t' ) {
				this.column = this.column + 4;
			// If it is not a '\t', increments the column by 1
			} else {
				this.column++;
			}
		}
	}
	
	/**
	 * Returns if a char is a digit (between 0 and 9)
	 * @param c
	 * @return
	 */
	private boolean isDigit(char c) {
		if ( c >= '0' && c <= '9' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a letter (between a and z or between A and Z)
	 * @param c
	 * @return
	 */
	private boolean isLetter(char c) {
		if ( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Returns if a char is a graphic (any ASCII visible character)
	 * @param c
	 * @return
	 */
	private boolean isGraphic(char c) {
		if ( c >= ' ' && c <= '~' ) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Scans the next token
	 * Simulates the DFA that recognizes the language described by the lexical grammar
	 * @return 
	 * @throws LexicalException
	 */ //TODO
	private int scanToken() throws LexicalException {
		// The initial automata state is 0
		// While loop to simulate the automata
		int estado = 0;
		
		while(true){
			switch(estado){
			case 0:
				//Estado 0 do AFD
				if(isLetter(this.currentChar)){
					estado = 1;
				}else if(isDigit(this.currentChar)){
					estado = 2;
				}else if(this.currentChar == '$'){
					estado = 3;
				}else if(this.currentChar == ':'){
					estado = 4;
				}else if(this.currentChar == '('){
					estado = 6;
				}else if(this.currentChar == ')'){
					estado = 7;
				}else if(this.currentChar == ','){
					estado = 8;
				}else if(this.currentChar == ';'){
					estado = 9;
				}else if(this.currentChar == '+' || this.currentChar == '-'){
					estado = 10;
				}else if(this.currentChar == '*'){
					estado = 11;
				}else if(this.currentChar == '/'){
					estado = 12;
				}else if(this.currentChar == '='){
					estado = 14;
				}else if(this.currentChar == '<' || this.currentChar == '>'){
					estado = 15;
				}else if(this.currentChar == '\000'){
					estado = 16;
				}else{
					//caso o caractere atual não seja nenhum dos caracteres acimas reconhecido pelo estado atual.
					throw new LexicalException("Caractere não esperado", this.currentChar, this.line, this.column);
				}
				getNextChar();
				break;
			case 1:
				//Estado 1 do AFD
				if(isLetter(this.currentChar) || isDigit(this.currentChar) || this.currentChar == '_'){
					//Caso seja uma letra ou numero ou underScore ele não muda de estado e pega o proximo caractere
					getNextChar();
				}else{
					//caso não seja um caractere reconhecido pelo estado atual,
					//verificar se o buffer é uma palavra reservada ou um id.
					if(this.currentSpelling.toString().equalsIgnoreCase("PROCEDURE")){
						return TokenType.PROCEDURE;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("IS")){
						return TokenType.IS;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("BEGIN")){
						return TokenType.BEGIN;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("END")){
						return TokenType.END;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("FUNCTION")){
						return TokenType.FUNCTION;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("RETURN")){
						return TokenType.RETURN;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("$")){
						return TokenType.$;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("IF")){
						return TokenType.IF;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("THEN")){
						return TokenType.THEN;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("ELSE")){
						return TokenType.ELSE;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("ELSIF")){
						return TokenType.ELSIF;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("ENDIF")){
						return TokenType.ENDIF;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("WHILE")){
						return TokenType.WHILE;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("LOOP")){
						return TokenType.LOOP;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("ENDLOOP")){
						return TokenType.ENDLOOP;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("BREAK")){
						return TokenType.BREAK;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("CONTINUE")){
						return TokenType.CONTINUE;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("INTEGER")){
						return TokenType.INTEGER;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("BOOLEAN")){
						return TokenType.BOOLEAN;
					}else if(this.currentSpelling.toString().equalsIgnoreCase("PUT_LINE")){
						return TokenType.PUT_LINE;
					}else{
						return TokenType.ID;
					}
				}
				break;

			case 2:
				//Estado 2 reconhece um número
				if(isDigit(this.currentChar)){
					getNextChar();
				}else{
					return TokenType.NUMERO; 
				}
				break;

			case 3:
				//Estado 3 reconhece um $ que é o simbolo inicial da chamada de função
				return TokenType.$;

			case 4:
				//Estado 4 transição para o estado 5 que reconhece o operador de atibuicao ":="
				//E final para o lexema ":"
				if(this.currentChar == '='){
					estado = 5;
					getNextChar();
				}else{
					return TokenType.DOISPONTOS;
				}
				break;

			case 5:
				//Estado 5 que reconhece o operador de Atribuicao
				return TokenType.OPATRIBUICAO;

			case 6:
				//estado 6 que reconhece o "("
				return TokenType.PARENTESESESQUERDO;

			case 7:
				//Estado 7 que reconhece o ")"
				return TokenType.PARENTESESDIREITO;

			case 8:
				//Estado 8 que reconhece o ","
				return TokenType.VIRGULA;

			case 9:
				//Estado 9 que reconhece o ";"
				return TokenType.PONTOEVIRGULA;

			case 10:
				//Estado 10 que reconhece os operadores "+" e "-"
				return TokenType.OPARIADI;

			case 11:
				//Estado 11 que reconhece o operador "*"
				return TokenType.OPARIMUL;

			case 12:
				//estado 12 que reconhece o operador "/" e que é transicao para o estado 13 que reconhece o OPREL("/=", "==", "<", ... )
				if(this.currentChar == '='){
					estado = 13;
					getNextChar();
				}else{
					return TokenType.OPARIMUL;
				}
				break;

			case 13:
				//Estado 13 que reconhece os operadores relacionais "/=", "==", "<", ">", "<=", ">="
				return TokenType.OPREL;

			case 14:
				//Estado 14 é transicao para o estado 13 dos OPREL
				//Ao passar por esse estado o OPREL é o ==
				if(this.currentChar == '='){
					estado = 13;
					getNextChar();
				}else{
					throw new LexicalException("Caractere não esperado", this.currentChar, this.line, this.column);
				}
				break;

			case 15:
				//Estado 15 que é transicao par o estado 13 dos OPREL
				// Ao passar por esse estado os OPREL podem ser "<", ">", "<=", ">="
				if(this.currentChar == '='){
					getNextChar();
				}
				estado = 13;
				break;

			case 16:
				//estado 16 reconhece o final do arquivo
				return TokenType.EOT;
			}
		}
	}
	
	
}
